namespace ODataValidator.RuleEngine.Common
{
    #region Namespaces
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Text.RegularExpressions;
    #endregion

    /// <summary>
    /// Helper class to get server response for given uri string and request Accept header value
    /// </summary>
    public static class WebHelper
    {
        /// <summary>
        /// Gets response content from uri string and accept header value
        /// </summary>
        /// <param name="uri">uri pointing to the destination resource</param>
        /// <param name="acceptHeader">value of Accept header in request</param>
        /// <param name="maximumPayloadSize">maximum size of payload in byte</param>
        /// <param name="reqHeaders">collection of Http header to be sent out</param>
        /// <param name="credential">credentials for the service or null in case the service doesn't need authentication</param>
        /// <returns>Reponse object which contains payload, response headers and status code</returns>
        /// <exception cref="ArgumentException">Throws exception when parameter is out of scope</exception>
        /// <exception cref="OversizedPayloadException">Throws exception when payload content exceeds the set maximum size</exception>
        public static Response Get(Uri uri, string acceptHeader, int maximumPayloadSize, IEnumerable<KeyValuePair<string, string>> reqHeaders, System.Net.NetworkCredential credential)
        {
            var req = WebRequest.Create(uri);
            var reqHttp = req as HttpWebRequest;

            if (!string.IsNullOrEmpty(acceptHeader) && reqHttp != null)
            {
                reqHttp.Accept = acceptHeader;
            }

            if (reqHeaders != null && reqHeaders.Any())
            {
                foreach (var p in reqHeaders)
                {
                    if (!string.IsNullOrEmpty(p.Key))
                    {
                        reqHttp.Headers[p.Key] = p.Value;
                    }
                }
            }

            return WebHelper.Get(req, maximumPayloadSize, credential);
        }

        /// <summary>
        /// Returns Reponse object from a WebRequest object
        /// </summary>
        /// <param name="request">WebRequest object for which the response is returned</param>
        /// <param name="maximumPayloadSize">maximum size of payload in byte</param>
        /// <param name="credential">credentials for the service or null in case the service doesn't need authentication</param>
        /// <returns>Reponse object which contains payload, response headers and status code</returns>
        /// <exception cref="ArgumentException">Throws exception when parameter is out of scope</exception>
        /// <exception cref="OversizedPayloadException">Throws exception when payload content exceeds the set maximum size</exception>
        [SuppressMessage("DataWeb.Usage", "AC0013: call WebUtil.GetResponseStream instead of calling the method directly on the HTTP object.", Justification = "interop prefers to interact directly with network")]
        public static Response Get(WebRequest request, int maximumPayloadSize, System.Net.NetworkCredential credential)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            try
            {
                if (credential != null)
                    request.Credentials = credential;

                using (WebResponse resp = request.GetResponse())
                {
                    string responseHeaders, responsePayload;
                    HttpStatusCode? statusCode = WebHelper.ParseResponse(maximumPayloadSize, resp, out responseHeaders, out responsePayload);
                    return new Response(statusCode, responseHeaders, responsePayload);
                }
            }
            catch (WebException wex)
            {
                try
                {
                    if (wex.Response != null)
                    {
                        string responseHeaders, responsePayload;
                        HttpStatusCode? statusCode = WebHelper.ParseResponse(maximumPayloadSize, wex.Response, out responseHeaders, out responsePayload);
                        return new Response(statusCode, responseHeaders, responsePayload);
                    }
                }
                catch (OversizedPayloadException)
                {
                    return new Response(null, null, null);
                }
            }
        
            return new Response(null, null, null);
        }

        /// <summary>
        /// Extracts status code, response headers and payload (as string) from a WebResponse object
        /// </summary>
        /// <param name="maximumPayloadSize">maximum size of payload in bytes</param>
        /// <param name="response">WebResponse object containing all the information about the response</param>
        /// <param name="responseHeaders">respone header block in WebResponse object</param>
        /// <param name="responsePayload">response payload in WebResponse object</param>
        /// <returns>http status code if http/https protocol is invloved; otherwise, null</returns>
        /// <exception cref="ArgumentException">Throws exception when parameter is out of scope</exception>
        /// <exception cref="OversizedPayloadException">Throws exception when payload content exceeds the set maximum size</exception>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", Justification = "response header and payload are returned values too")]
        [SuppressMessage("DataWeb.Usage", "AC0013: call WebUtil.GetResponseStream instead of calling the method directly on the HTTP object.", Justification = "interop prefers to interact directly woth network")]
        public static HttpStatusCode? ParseResponse(int maximumPayloadSize, WebResponse response, out string responseHeaders, out string responsePayload)
        {
            HttpStatusCode? statusCode = null;
            responseHeaders = null;
            responsePayload = null;

            if (response != null)
            {
                if (maximumPayloadSize <= 0)
                {
                    throw new ArgumentException(Resource.ArgumentNegativeOrZero, "maximumPayloadSize");
                }

                if (response.ContentLength > maximumPayloadSize)
                {
                    throw new OversizedPayloadException(string.Format(CultureInfo.CurrentCulture, Resource.formatPayloadSizeIsTooBig, response.ContentLength));
                }

                var headers = response.Headers;
                responseHeaders = headers != null ? headers.ToString() : null;

                string charset = null;
                var httpWebResponse = response as HttpWebResponse;
                if (httpWebResponse != null)
                {
                    charset = httpWebResponse.CharacterSet;
                    statusCode = httpWebResponse.StatusCode;
                }
                else
                {
                    charset = responseHeaders.GetCharset();
                }

                string contentType = responseHeaders.GetContentTypeValue();

                using (var stream = response.GetResponseStream())
                {
                    responsePayload = WebHelper.GetPayloadString(stream, maximumPayloadSize, charset, contentType);
                }
            }

            return statusCode;
        }

        /// <summary>
        /// Canonicalizes a uri
        /// </summary>
        /// <param name="uri">uri to be canonicalized</param>
        /// <returns>the canonicalized uri</returns>
        public static Uri Canonicalize(this Uri uri)
        {
            Uri canonical = null;

            if (uri != null)
            {
                try
                {
                    string safeUnescaped = uri.GetComponents(UriComponents.AbsoluteUri, UriFormat.SafeUnescaped);
                    string normalized = Regex.Replace(safeUnescaped, "(?<!:)/+", "/");
                    canonical = new Uri(normalized);
                }
                catch (UriFormatException)
                {
                    // does nothing
                }
            }

            return canonical;
        }

        /// <summary>
        /// Reads the payload with the specified charset or embedded encoding in xml block if applicable
        /// </summary>
        /// <param name="stream">payload stream</param>
        /// <param name="maximumPayloadSize">maximum payload size in bytes</param>
        /// <param name="charset">charset value in content-type header</param>
        /// <param name="contentType">content-type value</param>
        /// <returns>payload text read using proper encoding</returns>
        private static string GetPayloadString(Stream stream, int maximumPayloadSize, string charset, string contentType)
        {
            byte[] buffer = WebHelper.GetPayloadBytes(stream, maximumPayloadSize);
            if (buffer == null || buffer.Length == 0)
            {
                return null;
            }

            Encoding encoding = Encoding.Default;
            if (!string.IsNullOrEmpty(charset))
            {
                try
                {
                    encoding = Encoding.GetEncoding(charset);
                }
                catch (ArgumentException)
                {
                    charset = null;
                }
            }

            string responsePayload = encoding.GetString(buffer);

            if (string.IsNullOrEmpty(charset))
            {
                string payloadInEmbbedEncoding;
                if (TryReadWithEmbeddedEncoding(buffer, contentType, responsePayload, out payloadInEmbbedEncoding))
                {
                    return payloadInEmbbedEncoding;
                }
            }

            return responsePayload;
        }

        /// <summary>
        /// Reads stream into a byte array while limiting the size to the specified maximum 
        /// </summary>
        /// <param name="stream">stream obhect to read from</param>
        /// <param name="maximumPayloadSize">the maximum number of bytes to read</param>
        /// <returns>byte array</returns>
        /// 
        [SuppressMessage("Microsoft.MSInternal", "CA908:generic method that does not require JIT compilation at runtime", Justification = "no other way to strip byte[] yet")]
        private static byte[] GetPayloadBytes(Stream stream, int maximumPayloadSize)
        {
            if (stream == null)
            {
                return null;
            }

            byte[] buffer = new byte[maximumPayloadSize];
            int roomLeft = maximumPayloadSize;
            int offset = 0;

            int count;
            while ((count = stream.Read(buffer, offset, roomLeft)) > 0)
            {
                offset += count;
                roomLeft -= count;
            }

            if (roomLeft == 0 && stream.ReadByte() != -1)
            {
                throw new OversizedPayloadException(Resource.PayloadSizeIsTooBig);
            }

            Array.Resize<byte>(ref buffer, offset);
            return buffer;
        }

        /// <summary>
        /// Try to read byte arrary into a string using the possibly-existent encoding seeting within
        /// </summary>
        /// <param name="buffer">byte arrary to read from</param>
        /// <param name="contentType">content type of the byte array</param>
        /// <param name="contentHint">string of byte array in the default encoding</param>
        /// <param name="content">output string of byte array in the embedded encoding</param>
        /// <returns>true if content is read using a found valid encoding; otherwise false</returns>
        [SuppressMessage("Microsoft.Globalization", "CA1308: replace the call to 'string.ToLowerInvariant()' with String.ToUpperInvariant()'.", Justification = "the comparation targets are in lower case")]
        private static bool TryReadWithEmbeddedEncoding(byte[] buffer, string contentType, string contentHint, out string content)
        {
            if (!string.IsNullOrEmpty(contentType))
            {
                contentType = contentType.ToLowerInvariant();
                switch (contentType)
                {
                    case Constants.ContentTypeXml:
                    case Constants.ContentTypeAtom:
                    case Constants.ContentTypeTextXml:
                    case Constants.ContentTypeJson:
                        return TryReadXmlUsingEmbeddedEncoding(buffer, contentHint, out content);
                }
            }

            content = null;
            return false;
        }

        /// <summary>
        /// Try to read XML content using the embedded encoding
        /// </summary>
        /// <param name="buffer">byte array of the XML literal</param>
        /// <param name="xmlHint">string of the xml literal in the default encoding</param>
        /// <param name="xml">output string of the xml literal in the embedded encoding</param>
        /// <returns>true if the output is read using a valid embedded encoding; otherwise false</returns>
        public static bool TryReadXmlUsingEmbeddedEncoding(byte[] buffer, string xmlHint, out string xml)
        {
            string charsetEmbedded = null;
            charsetEmbedded = xmlHint.GetEmbeddedEncoding();

            if (!string.IsNullOrEmpty(charsetEmbedded))
            {
                try
                {
                    Encoding encoding = Encoding.GetEncoding(charsetEmbedded);
                    xml = encoding.GetString(buffer);
                    return true;
                }
                catch (ArgumentException)
                {
                    // does nothing
                }
            }

            xml = null;
            return false;
        }
    }
}
